package probe

import (
	"encoding/json"
	probeOp "github.com/Unixeno/TheMoon/common/probe"
	"github.com/Unixeno/TheMoon/logging"
	"github.com/Unixeno/TheMoon/model"
	"github.com/Unixeno/TheMoon/moon/core/context"
	"github.com/Unixeno/TheMoon/moon/core/module"
	"github.com/shirou/gopsutil/cpu"
	"github.com/shirou/gopsutil/disk"
	"github.com/shirou/gopsutil/host"
	"github.com/shirou/gopsutil/load"
	"github.com/shirou/gopsutil/mem"
	"github.com/shirou/gopsutil/net"
	"io/ioutil"
	"strings"
	"time"
)

const ModuleName = "probe"

var logger = logging.GetLogger(ModuleName)

const targetModule = "collector"

type Probe struct {
	Interval         time.Duration
	Interfaces       []string // 需要监控的网卡列表
	StorageDevice    []string // 需要监控的块设备列表
	interfaceHistory map[string]model.NetStatistics
	diskHistory      map[string]DiskHistory
}

type DiskHistory struct {
	BytesRead  uint64
	BytesWrite uint64
	IOTime     uint64 // io读写操作等待时间，单位毫秒
}

func GetModule() module.Module {
	return &Probe{
		Interval:      time.Second,
		Interfaces:    []string{},
		StorageDevice: []string{},
	}
}

func (probe *Probe) Init() {
	logger.Debug("init probe")
	context.RegisterModule(ModuleName)
	probe.initNetState()
	probe.initDiskStat()
	// 启动时需要初始化响应的统计计数器
	probe.getStats()

	probe.updateNode()
}

func (probe *Probe) updateNode() {
	// 从汇报节点软硬件信息时获取统计间隔等配置
	i := GetNodeInfo()
	data, _ := json.Marshal(i)

	msg := model.NewMessage(ModuleName, targetModule)
	msg.Operation = probeOp.OpUpdateSettings
	msg.Content = data

	retMsg, err := context.SyncSendMessage(msg, time.Second*3)
	if err != nil {
		logger.WithError(err).Error("failed to sync settings")
	} else {
		probe.handleUpdateSettings(retMsg)
	}
}

func (probe *Probe) Start() {
	logger.Debug("start probe")

	go func() {
		for {
			msg, err := context.RecvMessage(ModuleName)
			if err != nil {
				return
			}
			if msg.Operation != probeOp.OpUpdateSettings {
				logger.Warning("unknown message operation")
			} else {
				probe.handleUpdateSettings(msg)
			}
		}
	}()

	go func() {
		for {
			select {
			case <-context.Done():
				return
			case <-time.After(probe.Interval):
				s := probe.getStats()
				probe.sendStats(s)
			}
		}
	}()
}

func (probe *Probe) handleUpdateSettings(msg model.Message) {
	var settings model.StatisticsSettings
	if err := json.Unmarshal(msg.Content.([]byte), &settings); err != nil {
		logger.Warning("invalid message format")
	} else {
		probe.Interval = time.Second * time.Duration(settings.Interval)
		logger.Info("stats settings updated")
	}
}

func (probe *Probe) sendStats(statistics *model.Statistics) {
	message := model.NewMessage(ModuleName, targetModule)

	data, err := json.Marshal(statistics)
	if err != nil {
		logger.WithError(err)
		return
	}
	message.Content = data
	message.Operation = probeOp.OpReport
	err = context.SendMessage(message)
	if err != nil {
		logger.WithError(err).Error("failed to send statistics data")
	}
}

func (probe *Probe) getStats() *model.Statistics {
	statistics := &model.Statistics{}
	if cpuPercent, err := cpu.Percent(0, false); err != nil {
		logger.WithError(err).Error()
	} else {
		statistics.CPUPercent = float32(cpuPercent[0])
	}
	if memStat, err := mem.VirtualMemory(); err != nil {
		logger.WithError(err).Error()
	} else {
		statistics.MemoryPercent = float32(memStat.UsedPercent)
		statistics.MemoryAvailable = memStat.Available
	}

	if b, err := host.Uptime(); err != nil {
		logger.WithError(err).Error()
	} else {
		statistics.UpTime = uint32(b)
	}

	if sysLoad, err := load.Avg(); err != nil {
		logger.WithError(err).Error()
	} else {
		statistics.Load1 = float32(sysLoad.Load1)
		statistics.Load5 = float32(sysLoad.Load5)
		statistics.Lode15 = float32(sysLoad.Load15)
	}
	probe.netStats(statistics)
	probe.diskStats(statistics)
	return statistics
}

func (probe *Probe) GetInterfaceList() []string {
	result := make([]string, 0, 2)
	if stats, err := net.IOCounters(true); err == nil {
		for _, val := range stats {
			result = append(result, val.Name)
		}
	}
	return result
}

func (probe *Probe) GetBlkDeviceList() (nameList []string) {
	nameList = make([]string, 0)

	blockDeviceDir := "/sys/block"
	files, err := ioutil.ReadDir(blockDeviceDir)
	if err != nil {
		logger.WithError(err).Error("failed to get block devices on this system")
		return
	}
	for _, file := range files {
		deviceName := file.Name()
		if strings.HasPrefix(deviceName, "loop") {
			continue
		}
		nameList = append(nameList, deviceName)
	}
	return nameList
}

func (probe *Probe) initNetState() {
	probe.interfaceHistory = make(map[string]model.NetStatistics)
	if len(probe.Interfaces) == 0 {
		probe.Interfaces = probe.GetInterfaceList()
	}
	for _, name := range probe.Interfaces {
		probe.interfaceHistory[name] = model.NetStatistics{}
	}
}

func (probe *Probe) netStats(statistics *model.Statistics) {
	if stats, err := net.IOCounters(true); err != nil {
		return
	} else {
		statistics.NetStat = make(map[string]model.NetStatistics)
		for _, interfaceStat := range stats {
			if _, ok := probe.interfaceHistory[interfaceStat.Name]; ok {
				// 需要统计的接口
				statistics.NetStat[interfaceStat.Name] = model.NetStatistics{
					BytesSent:      interfaceStat.BytesSent,
					BytesRecv:      interfaceStat.BytesRecv,
					BytesSentDelta: interfaceStat.BytesSent - probe.interfaceHistory[interfaceStat.Name].BytesSent,
					BytesRecvDelta: interfaceStat.BytesRecv - probe.interfaceHistory[interfaceStat.Name].BytesRecvDelta,
				}
			}
		}
		probe.interfaceHistory = statistics.NetStat
	}
}

func (probe *Probe) initDiskStat() {
	if len(probe.StorageDevice) == 0 {
		probe.StorageDevice = probe.GetBlkDeviceList()
	}
	probe.diskHistory = make(map[string]DiskHistory)
	for _, name := range probe.StorageDevice {
		probe.diskHistory[name] = DiskHistory{}
	}
}

func (probe *Probe) diskStats(statistics *model.Statistics) {
	stat, err := disk.IOCounters(probe.StorageDevice...)
	if err != nil {
		logger.WithError(err).Error()
	}
	statistics.DiskStat = make(map[string]model.DiskStatistics)
	for name, current := range stat {
		statistics.DiskStat[name] = model.DiskStatistics{
			BytesReadDelta:  current.ReadBytes - probe.diskHistory[name].BytesRead,
			BytesWriteDelta: current.WriteBytes - probe.diskHistory[name].BytesWrite,
			IOPercent: calcDiskPercent(probe.diskHistory[name].IOTime,
				current.WriteTime+current.ReadTime, int(probe.Interval)),
		}
		probe.diskHistory[name] = DiskHistory{
			BytesRead:  current.ReadBytes,
			BytesWrite: current.WriteBytes,
			IOTime:     current.ReadTime + current.WriteTime,
		}
	}
}

func calcDiskPercent(lastTime, currentTime uint64, interval int) float32 {
	percent := float64(currentTime-lastTime) / 10 / float64(interval)
	if percent > 99.9 {
		percent = 100
	} else if percent < 0.1 {
		percent = 0
	}
	return float32(percent)
}
